<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>The Animation Aemonstrates The Process</title>
  </head>
  <style>
    body {
      font-family: Consolas, "Courier New", monospace;
      font-size: 24px;
      min-width: 1080px;
      background-color: #333;
      color: #fff;
    }
    header {
      width: 100%;

      p {
        margin-block-start: 1.2rem;
        margin-block-end: 1.2rem;
        text-align: center;
        font-size: 2rem;
        line-height: 2rem;
        font-weight: bolder;

        input {
          width: 36rem;
          font-size: 2rem;
          line-height: 2rem;
          text-align: center;
          border-radius: 0.4rem;
          letter-spacing: 0.4rem;
        }
      }
    }
    footer {
      margin: 0 auto;
      min-height: 4em;
      width: 1080px;
      word-break: break-word;
      word-wrap: break-word;
    }

    svg#ANIMATOR {
      display: block;
      margin: 0 auto;
      width: 1080px;
      height: 360px;
      background-color: #fff;
      border-radius: 6px;

      .upper {
        width: 80px;
        height: 80px;
        z-index: 1;
        stroke: #f33;
        stroke-width: 8px;
        fill: #fff;

        &[val="0"] {
          fill: #fff;
        }
        &[val="1"] {
          fill: #666;
        }
        &[val="-1"] {
          fill: #0000;
          stroke: #0000;
        }
      }

      .lower {
        width: 72px;
        height: 64px;
        z-index: 2;
        stroke: #666;
        stroke-width: 4px;
        fill: #666;

        &[val="0"] {
          opacity: 0;
        }
        &[val="1"] {
          opacity: 1;
        }
      }

      .plate {
        width: 80px;
        height: 12px;
        z-index: 1;
        stroke: #333;
        stroke-width: 2px;
        fill: #333;

        &[val="0"] {
          opacity: 0;
        }
        &[val="1"] {
          opacity: 1;
        }
      }
    }
  </style>
  <body>
    <header>
      <p>
        <input id="iDec" type="number" placeholder="Enter Decimal Integer" />
      </p>
      <!-- <p>
        <input id="iTer" readonly placeholder=" The Ternary Integer  " />
      </p> -->
    </header>

    <main>
      <svg
        id="ANIMATOR"
        viewBox="0 0 1080 360"
        preserveAspectRatio="xMidYMid meet"
      >
        <g id="UPPER_STACK"></g>
        <g id="LOWER_STACK"></g>
        <g id="PLATE_STACK"></g>
      </svg>
    </main>

    <footer id="ft"></footer>
  </body>
  <script>
    "use strict";
    // 隐藏偏移量
    const HIDE_LENGTH = 1 << 16;
    // 块宽
    const TYTE_WITH = 100;
    // 消退时长
    const CLEAR_DUR = 100;
    // 移动时长
    const MOVE_DUR = 300;

    // 创建SVG元素
    const createSVGelm = (type = "rect") =>
      document.createElementNS("http://www.w3.org/2000/svg", type);

    // 十进制输入框
    const iDec = document.getElementById("iDec");
    // 三进制输出框
    // const iTer = document.getElementById("iTer");
    // 底部输出
    const ft = document.getElementById("ft");

    // 全局变量三进制字符串
    var intTStr = "";

    // 十进制输入监听，转换为三进制，并触发后续操作
    iDec.addEventListener("change", async () => {
      let dec = parseInt(iDec.value);
      if (dec < 0 || isNaN(dec)) {
        return alert("Invalid Input");
      } else if (dec > 60000) {
        return alert("Input too large");
      }
      intTStr = dec.toString(3);

      await Tyte.init();
      await Tyte.run();
    });

    const ANIMATOR = document.getElementById("ANIMATOR");

    /**
     *
     * SVG control element class, used for creating and controlling SVG elements in the animator.
     *
     */
    class SVGctrlElm {
      static stack = ANIMATOR;

      /**
       * 根据传入的参数获取一个对象实例。
       * 此函数旨在实现对象的重用，通过从一个预定义的父节点中获取可用的对象，或者在没有可用对象时创建新对象。
       * 这种做法有助于减少内存分配和垃圾收集的开销，特别是在频繁创建和销毁对象的场景中。
       *
       * @param {Object} param0 函数的参数对象。
       * @param {HTMLElement} param0.parentNode - 默认为ANIMATOR，表示对象的父节点。这个父节点用于存储和管理对象实例。
       * @param {Function} param0.cls - 默认为SVGctrlElm，表示对象的构造函数。这个构造函数用于创建新对象。
       * @param {number} param0.val - 默认为0，表示对象的值。这个值可以被设置到重新使用或新创建的对象上。
       * @param {number} param0.idx - 默认为0，表示对象的索引。这个索引可以被设置到重新使用或新创建的对象上。
       * @returns {Object} 返回一个SVGctrlElm类型的对象实例。
       */
      static getObject({
        stack = ANIMATOR,
        parentNode = ANIMATOR,
        cls = SVGctrlElm,
        val = 0,
        idx = 0,
      }) {
        // 检查栈中是否有可用的对象
        if (stack?.children?.[0]?.obj instanceof cls) {
          // 从栈中获取第一个对象
          let obj = stack.children[0].obj;
          // 设置对象的值为传入的参数值
          obj.val = val;
          obj.idx = idx;
          obj.elm.setAttribute("opacity", 1);

          // 将对象从栈中移除并添加到父节点
          stack.removeChild(stack.children[0]);
          parentNode.appendChild(obj.elm);

          // 返回复用的对象
          return obj;
        } else {
          // 栈为空时，创建新对象
          return new cls({ parentNode, val, idx });
        }
      }

      /**
       * Constructor, creates an SVG element.
       * @param {HTMLElement} parentNode - The parent node where the SVG element will be appended, default is the animator.
       * @param {string} type - The type of SVG element to create, default is "rect" (rectangle).
       */
      constructor({ parentNode = ANIMATOR, type = "rect", val = 0, idx = 0 }) {
        this.parentNode = parentNode;
        this.elm = createSVGelm(type);
        this.parentNode.appendChild(this.elm);
        this.val = val;
        this.idx = idx;
        this.elm.obj = this;
        this.elm.classList.add(this.constructor.name.toLowerCase());
      }

      get x() {
        return parseInt(this.elm.getAttribute("x")) || 0;
      }

      set x(x) {
        if (x === this.x) return null;
        this.elm.setAttribute("x", x);
      }

      get y() {
        return parseInt(this.elm.getAttribute("y")) || 0;
      }

      set y(y) {
        if (y === this.y) return null;
        this.elm.setAttribute("y", y);
      }

      get val() {
        return this._val;
      }

      set val(v) {
        if (v === this._val) return null;
        this._val = v;
        this.elm.setAttribute("val", v);
      }

      get idx() {
        return this._idx;
      }

      set idx(i) {
        if (i === this._idx) return null;
        this._idx = i;
        this.elm.setAttribute("idx", i);
      }

      /**
       * Initiates an animation request.
       * @param {Object} options - Animation configuration options.
       * @param {Function} options.initFn - The function to execute at the beginning of the animation.
       * @param {Function} options.runFn - The function to execute during the animation, receives the animation progress as a parameter.
       * @param {Function} options.endFn - The function to execute at the end of the animation.
       * @param {number} options.duration - The duration of the animation in milliseconds, default is 100.
       * @returns {Promise} Returns a Promise that resolves when the animation ends.
       */
      async requestAnimation({
        initFn = () => {},
        runFn = () => {},
        endFn = () => {},
        duration = 100,
      }) {
        if (!this.elm) return null;

        return new Promise((rsl, rjt) => {
          let startTime;

          // parameter: time will auto inject by requestAnimationFrame
          const render = (time) => {
            if (!startTime) {
              startTime = time;
              initFn();
            }

            // calculate running progress
            const progress = (time - startTime) / duration;

            // request next frame while not reach 100%
            if (progress < 1) {
              window.requestAnimationFrame(render);

              // do animation here
              runFn(progress);
            } else {
              endFn();
              // Promise result
              rsl(true);
            }
          };

          // animation begin.
          window.requestAnimationFrame(render);
        });
      }

      /**
       * Clears the SVG element by animating its opacity to 0 and then removing it from the parent node.
       * @param {number} duration - The duration of the clear animation in milliseconds, default is 100.
       * @returns {Promise} Returns a Promise that resolves after the element is cleared.
       */
      async clear(stack = ANIMATOR, duration = CLEAR_DUR) {
        return this.requestAnimation({
          duration,
          runFn: (progress) => {
            this.elm.setAttribute("opacity", 1 - progress);
          },
          endFn: () => {
            this.elm.setAttribute("opacity", 0);
            this.x = HIDE_LENGTH;
            this.y = HIDE_LENGTH;
            this.elm.parentNode.removeChild(this.elm);
            stack.appendChild(this.elm);
          },
        });
      }

      /**
       * 将元素移动到指定的位置。
       *
       * @param {number} bX 目标元素的横坐标。
       * @param {number} bY 目标元素的纵坐标。
       * @param {number} duration 移动过程的持续时间，单位为毫秒。
       */
      async moveTo(bX = 0, bY = 0, duration = MOVE_DUR) {
        const [x0, y0] = [this.x, this.y];
        // 计算元素需要移动的横向和纵向距离。
        const [dX, dY] = [bX - x0, bY - y0];
        // 请求动画帧，逐步移动元素到目标位置。
        return this.requestAnimation({
          duration,
          runFn: (progress) => {
            // 根据移动进度更新元素的横纵坐标。
            this.x = x0 + dX * progress;
            this.y = y0 + dY * progress;
          },
          endFn: () => {
            // 动画结束时，确保元素的坐标更新为目标坐标。
            this.x = bX;
            this.y = bY;
          },
        });
      }

      /**
       * 将元素相对当前位置移动指定的距离。
       *
       * @param {number} dX 元素需要增加的横坐标距离。
       * @param {number} dY 元素需要增加的纵坐标距离。
       * @param {number} duration 移动过程的持续时间，单位为毫秒。
       */
      async moveBy(dX = 0, dY = 0, duration = MOVE_DUR) {
        const [x0, y0] = [this.x, this.y];
        // 获取当前元素的横坐标和纵坐标，如果不存在则默认为0。

        // 请求动画帧，逐步移动元素到目标位置。
        return this.requestAnimation({
          duration,
          runFn: (progress) => {
            // 根据移动进度更新元素的横纵坐标。
            this.x = x0 + dX * progress;
            this.y = y0 + dY * progress;
          },
          endFn: () => {
            // 动画结束时，确保元素的坐标相对于初始位置增加了指定的距离。
            this.x += dX;
            this.y += dY;
          },
        });
      }
    }

    /**
     * 继承自SVGctrlElm，用于创建上方元素SVG控制类。
     * 用单例进行控制
     */
    class Upper extends SVGctrlElm {
      static Y_BIAS = 28;
      static X_BIAS = 10;

      static array = [];
      static stack = document.getElementById("UPPER_STACK");

      static getObject({ parentNode = Upper.stack, val = 0, idx = 0 }) {
        return SVGctrlElm.getObject({
          stack: Upper.stack,
          parentNode: parentNode,
          cls: Upper,
          val: val,
          idx: idx,
        });
      }

      constructor({ parentNode = ANIMATOR, val = 0, idx = 0 }) {
        super({ parentNode, type: "rect", val, idx });
      }

      async aline(idx = this._idx, duration = MOVE_DUR) {
        return this.moveTo(
          idx * TYTE_WITH + Upper.X_BIAS,
          Upper.Y_BIAS,
          duration
        );
      }

      async clear(duration = CLEAR_DUR) {
        return super.clear(Upper.stack, duration);
      }
    }

    /**
     * 继承自SVGctrlElm，用于创建下方元素SVG控制类。
     */
    class Lower extends SVGctrlElm {
      static X_BIAS = 14;
      static U_Y = 76;
      static A_Y = 136;
      static B_Y = 228;

      static array = [];
      static stack = document.getElementById("LOWER_STACK");

      static getObject({
        parentNode = Lower.stack,
        val = 0,
        idx = 0,
        pos = 0,
      }) {
        let obj = SVGctrlElm.getObject({
          stack: Lower.stack,
          parentNode: parentNode,
          cls: Lower,
          val: val,
          idx: idx,
        });
        obj.pos = pos;
        return obj;
      }

      constructor({ parentNode = Lower.stack, val = 0, idx = 0, pos = 0 }) {
        super({ parentNode, type: "rect", val, idx });
        this._pos = pos;
      }

      get pos() {
        return this._pos;
      }

      set pos(c) {
        this._pos = c;
        this.elm.setAttribute("pos", this._pos);
      }

      async aline(idx = this._idx) {
        let y = this._pos ? Lower.B_Y : Lower.A_Y;
        return this.moveTo(idx * TYTE_WITH + Lower.X_BIAS, y, 0);
      }

      async clear(duration = CLEAR_DUR) {
        return super.clear(Lower.stack, duration);
      }

      async liftU(duration = MOVE_DUR) {
        return this.moveTo(this.x, Lower.U_Y, duration);
      }

      async fallA(lift = true, duration = MOVE_DUR) {
        if (lift) await this.liftU(0);
        return this.moveTo(this.x, Lower.A_Y, duration);
      }

      async fallB(lift = true, duration = MOVE_DUR) {
        if (lift) await this.liftU(0);
        return this.moveTo(this.x, Lower.B_Y, duration);
      }
    }

    /**
     * `Plate`类继承自`SVGctrlElm`，用于创建底盘SVG控制类。
     */
    class Plate extends SVGctrlElm {
      static X_BIAS = 10;
      static Y_BIAS = 312;

      static array = [];
      static stack = document.getElementById("PLATE_STACK");

      static getObject({ parentNode = Plate.stack, val = 0, idx = 0 }) {
        return SVGctrlElm.getObject({
          stack: Plate.stack,
          parentNode: parentNode,
          cls: Plate,
          val: val,
          idx: idx,
        });
      }

      constructor({ parentNode = Plate.stack, val = 0, idx = 0 }) {
        super({ parentNode, type: "rect", val, idx });
      }

      async aline(idx = this._idx) {
        return this.moveTo(idx * TYTE_WITH + Plate.X_BIAS, Plate.Y_BIAS, 0);
      }

      async clear(duration = 100) {
        return super.clear(Plate.stack, duration);
      }
    }

    /**
     * `Tyte`类用于创建一个Tyte对象，该对象包含一个上、两个下、一个底盘元素。
     */
    class Tyte {
      static array = [];
      static valMap = {
        // 0: { upper: 0, lowerA: 0, lowerB: 0, plate: 1 },
        //  6: { upper:0, lowerA:0, lowerB:0, plate:0 },
        0: [0, 0, 0, 1],
        1: [0, 0, 1, 1],
        2: [0, 1, 1, 1],
        3: [1, 0, 0, 1],
        4: [1, 0, 1, 1],
        5: [1, 1, 1, 1],
      };

      static async init() {
        ft.textContent = intTStr;

        // 清空原队列
        await Promise.all(Tyte.array.map(async (t) => await t.clear(10)));
        Tyte.array.length = 0;

        // 在Tyte.constructor中已添加入队列array
        // 无需再标注入队
        return await Promise.all(
          [...intTStr]
            .map((s) => new Tyte(s))
            .map(async (t, i) => await t.aline(i))
        );
      }

      static async run() {
        // --------- 用循环非递归 ---------
        while (Tyte.array.length >= 2) {
          let t0 = Tyte.array[0];

          if (t0.val === 0) {
            await Tyte.array[0].clear();
            Tyte.array.shift();
            // --------- 全对齐 ---------
            await Tyte.alineAll();
            // --------- 再循环 ----------
            continue;
          }

          // ---------- 缓存队尾 upper ----------
          let un = Tyte.array.at(-1)?.upper;

          // ---------- upper 位移 ----------
          for (let i = Tyte.array.length - 1; i >= 1; i--) {
            let u = Tyte.array[i - 1].upper;
            if (!u) continue;
            Tyte.array[i].removeUpper();
            Tyte.array[i].addUpper(u);
          }

          // ---------- 每轮循环队首插入 upper ----------
          t0.removeUpper();
          t0.addUpper(
            Upper.getObject({ parentNode: ANIMATOR, val: 0, idx: 0 })
          );

          await Promise.all(
            Tyte.array
              .map(async (t, i) => await t.upper?.aline(i))
              .concat((async () => await un?.aline(Tyte.array.length))())
          );

          // --------- 队中计算 及 队末加 2 ---------
          await Promise.all(
            Tyte.array
              .map(async (t) => await t.handleLowers())
              .concat(
                (async () => {
                  if (!un) return null;
                  // ---------- 按条件插入“2” ----------
                  if (un.val === 1) {
                    let tn = new Tyte(2);
                    await tn.aline(Tyte.array.length - 1);
                  }
                  // ---------- Upper回栈 ----------
                  return await un.clear();
                })()
              )
          );

          // ---------- 输出 ----------
          ft.textContent += ` -> ${Tyte.array
            .map((t) => t.val)
            .join("")
            .replace(/^0+/, "")}`;
        }

        return null;
      }

      static async alineAll() {
        return await Promise.all(
          Tyte.array.map(async (t, i) => {
            t.freshIdx(i);
            return await t.aline(i);
          })
        );
      }
      
      constructor(val) {
        this._val = val;
        // Tyte对象必须要进数组,未使用的Tyte对象需要销毁
        Tyte.array.push(this);
        this._idx = Tyte.array.length - 1;

        let pVals = Tyte.valMap[val];

        // Upper对象并不要求初始化时就创建
        this.upper = null;

        this.lowerA = Lower.getObject({
          parentNode: ANIMATOR,
          val: pVals[1],
          idx: this._idx,
          pos: 0,
        });

        this.lowerB = Lower.getObject({
          parentNode: ANIMATOR,
          val: pVals[2],
          idx: this._idx,
          pos: 1,
        });

        // Lower.array.push([this.lowerA, this.lowerB]);

        this.plate = Plate.getObject({
          parentNode: ANIMATOR,
          val: 1,
          idx: this._idx,
        });

        // Plate.array.push(this.plate);

        // 方便获取所有部件
        this.parts = [this.lowerA, this.lowerB, this.plate];
      }

      get val() {
        this._val =
          parseInt(this.lowerA.val) +
          parseInt(this.lowerB.val) +
          3 * parseInt(this.upper?.val || 0);
        return this._val;
      }

      get idx() {
        return this._idx;
      }

      freshIdx(idx) {
        this._idx = idx;
        this.parts.forEach((p) => (p.idx = idx));
      }

      addUpper(upper) {
        this.upper = upper;
        this.upper.idx = this.idx;
        this.parts.push(this.upper);
      }

      removeUpper() {
        this.parts = this.parts.filter((p) => p !== this.upper);
        this.upper = null;
      }

      async aline(idx) {
        return await Promise.all(
          this.parts.map(async (p) => await p.aline(idx))
        );
      }

      async shiftTyte(width = TYTE_WITH, duration = MOVE_DUR) {
        return await Promise.all(
          this.parts.map(async (p) => await p.moveBy(-1 * width, 0, duration))
        );
      }

      async clear(duration = CLEAR_DUR) {
        return await Promise.all(
          this.parts.map(async (p) => await p.clear(duration))
        );
      }

      async handleLowers(duration = MOVE_DUR) {
        if (!this.upper) return null;

        if (this.upper.val === 0) {
          if (this.lowerA.val === 0 && this.lowerB.val === 0) {
            return null;
          }

          if (this.lowerA.val === 0 && this.lowerB.val === 1) {
            await this.lowerB.liftU(true, duration);
            this.upper.val = 1;
            this.lowerB.val = 0;
            return null;
          }

          if (this.lowerA.val === 1 && this.lowerB.val === 1) {
            await this.lowerA.fallB(false, duration);
            this.lowerA.val = 0;
            return null;
          }
        } else if (this.upper.val === 1) {
          if (this.lowerA.val === 0 && this.lowerB.val === 0) {
            this.lowerB.val = 1;
            await this.lowerB.fallB(true, duration);
            return null;
          }

          if (this.lowerA.val === 0 && this.lowerB.val === 1) {
            this.lowerA.val = 1;
            await this.lowerA.fallA(true, duration);
            this.upper.val = 0;
            return null;
          }

          if (this.lowerA.val === 1 && this.lowerB.val === 1) {
            return null;
          }
        }
      }
    }
  </script>
</html>
